package com.parser.apk;

import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import com.parser.apk.AndroidXML.ResChunkHeader;
import com.parser.apk.AndroidXML.ResStringContent;

import static com.parser.apk.AndroidXML.*;

/**
 * Created by admin on 2017/12/5.
 */
public class APKParser {
    private static final String TARGET_MANIFEST_FILENAME = "Target_AndroidManifest.xml";
    private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
    private static final String MANIFEST_FILENAME = "AndroidManifest.xml";
    private static final String[] DIMENSION_UNITS = { "px","dip","sp","pt","in","mm","","" };
    private static final float RADIX_MULTS[] = { 0.00390625F,3.051758E-005F,1.192093E-007F,4.656613E-010F };
    private static final int BUFFER_SIZE = 8192;
    private static final int MAX_BUFFER_SIZE = 8 * BUFFER_SIZE;
    private List<ResStringContent> mStringPoolList;

    private boolean mIsHeaderFlag = true;
    private int mTableCharDepth = 0;
    private String mPrefix;
    private String mUri;

    public APKCommonInfo getAPKCommonInfo(String apkPath) {
        APKCommonInfo info = new APKCommonInfo();
        info.setPermissionList(new ArrayList<>());

        File srcFile = new File(apkPath);
        if (srcFile.exists()) {
            File manifest = decompressAPKFile(srcFile);
            if (manifest != null) {
                deleteTargetFileBeforeParse();
                parseAndroidManifest(manifest, info);
            }
        }

        return info;
    }

    private ResChunkHeader getChunkHeader(BufferedInputStream io, long skip, int headerSize) throws Throwable {
        byte[] header = new byte[headerSize];
        long offset = 0;
        io.mark(headerSize);
        if (skip > 0) {
            offset = io.skip(skip);
        }
        int readSize = io.read(header, 0, header.length);
        ResChunkHeader chunkHeader = new ResChunkHeader();
        if (readSize > 0) {
            chunkHeader.type = ByteUtil.bytesToShort(header, 0);
            chunkHeader.headerSize = ByteUtil.bytesToShort(header, 2);
            chunkHeader.size = ByteUtil.bytesToInteger(header, 4);
            //System.out.println("type : " + chunkHeader.type + ", headerSize : " + chunkHeader.headerSize + ", size : " + chunkHeader.size + ", offset : " + offset);
        }
        io.reset();

        return chunkHeader;
    }

    private void parseAndroidManifest(File manifest, APKCommonInfo info) {
        BufferedInputStream io = null;
        try {
            io = new BufferedInputStream(new FileInputStream(manifest), MAX_BUFFER_SIZE);
            long skip = 0;
            ResChunkHeader chunkHeader = getChunkHeader(io, skip, AndroidXML.CHUNK_HEADER_SIZE);
            int size = chunkHeader.size;
            while (skip < size) {
                chunkHeader = getChunkHeader(io, skip, AndroidXML.CHUNK_HEADER_SIZE);

                switch (chunkHeader.type) {
                    case AndroidXML.RES_XML_TYPE:
                        skip += chunkHeader.headerSize;
                        saveAndroidManifestInfo(XML_HEADER + "\n");
                        break;

                    case AndroidXML.RES_XML_RESOURCE_MAP_TYPE:
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_XML_START_NAMESPACE_TYPE:
                    case AndroidXML.RES_XML_END_NAMESPACE_TYPE:
                        parseXmlNameSpace(io, skip);
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_XML_START_ELEMENT_TYPE:
                        parseXmlStartElement(io, skip, info);
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_XML_END_ELEMENT_TYPE:
                        parseXmlEndElement(io, skip);
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_TABLE_TYPE:
                        skip += AndroidXML.TABLE_HEADER_SIZE;
                        break;

                    case AndroidXML.RES_STRING_POOL_TYPE:
                        if (mStringPoolList == null) {
                            mStringPoolList = new ArrayList<>();
                        }
                        getStringPoolHeader(io, skip, AndroidXML.STRING_POOL_HEADER_SIZE);
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_TABLE_PACKAGE_TYPE:
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_TABLE_TYPE_SPEC_TYPE:
                        skip += chunkHeader.size;
                        break;

                    case AndroidXML.RES_TABLE_TYPE_TYPE:
                        skip += chunkHeader.size;
                        break;

                    default:
                        break;
                }
            }
            //System.out.println("parse end");
        } catch (Throwable r) {
            r.printStackTrace();
        } finally {
            if (io != null) {
                try {
                    io.close();
                } catch (Throwable r) {
                    r.printStackTrace();
                }
            }
        }
    }

    private ResStringPoolHeader getStringPoolHeader(BufferedInputStream io, long skip, int headerSize) throws Throwable {
        boolean isUTF8;
        byte[] header = new byte[headerSize];
        long offset = 0;
        if (skip > 0) {
            offset = io.skip(skip);
        }
        int readSize = io.read(header, 0, header.length);
        ResStringPoolHeader stringPoolHeader = new ResStringPoolHeader();
        if (readSize > 0) {
            stringPoolHeader.type = ByteUtil.bytesToShort(header, 0);
            stringPoolHeader.headerSize = ByteUtil.bytesToShort(header, 2);
            stringPoolHeader.size = ByteUtil.bytesToInteger(header, 4);
            stringPoolHeader.stringCount = ByteUtil.bytesToInteger(header, 8);
            stringPoolHeader.styleCount = ByteUtil.bytesToInteger(header, 12);
            stringPoolHeader.flags = ByteUtil.bytesToInteger(header, 16);
            isUTF8 = (stringPoolHeader.flags & AndroidXML.UTF8_FLAG) > 0;
            stringPoolHeader.stringsStart = ByteUtil.bytesToInteger(header, 20);
            stringPoolHeader.stylesStart = ByteUtil.bytesToInteger(header, 24);
            /*System.out.println("type : " + stringPoolHeader.type
                    + ", headerSize : " + stringPoolHeader.headerSize
                    + ", size : " + stringPoolHeader.size
                    + ", offset : " + offset
                    + ", utf8 : " + isUTF8);*/

            int stringIndexSize = 0;
            if (stringPoolHeader.stringCount > 0) {
                io.skip(stringIndexSize = stringPoolHeader.stringCount * AndroidXML.STRING_POOL_REF_SIZE);
            }

            int styleIndexSize = 0;
            if (stringPoolHeader.styleCount > 0) {
                io.skip(styleIndexSize = stringPoolHeader.styleCount * AndroidXML.STRING_POOL_REF_SIZE);
            }

            byte[] stringPoolContent = new byte[stringPoolHeader.size - stringPoolHeader.headerSize - stringIndexSize - styleIndexSize];
            io.read(stringPoolContent, 0, stringPoolContent.length);
            int itemOffset = 0;
            for (int i = 0; i < stringPoolHeader.stringCount; ++i) {
                ResStringContent item = new ResStringContent();
                item.size = decodeStringLength(isUTF8, stringPoolContent, itemOffset);
                itemOffset += 2;
                item.content = new String(stringPoolContent, itemOffset, isUTF8 ? item.size : item.size * 2, isUTF8 ? Charset.forName("UTF-8") : Charset.forName("UTF-16LE"));
                itemOffset += isUTF8 ? item.size + 1 : (item.size + 1) * 2;
                mStringPoolList.add(item);
                //System.out.println(i + " : " + item.content + ", size : " + item.size);
            }
        }
        io.reset();

        return stringPoolHeader;
    }

    public short decodeStringLength(boolean isUTF8, byte[] buffer, int start) {
        if (isUTF8) {
            decodeUTF8Length(buffer, start);
            return decodeUTF8Length(buffer, start + 1);
        } else {
            return decodeLength(buffer, start);
        }
    }

    public short decodeUTF8Length(byte[] buffer, int start) {
        short len = (short) buffer[start];
        if ((len & 0x80) != 0) {
            len = (short) (((short)((len & 0x7F) << 8)) | ((short) buffer[start + 1]));
        }
        return len;
    }

    public short decodeLength(byte[] buffer, int start) {
        short len = ByteUtil.bytesToShort(buffer, start);
        if ((len & 0x8000) != 0) {
            len = (short) (((len & 0x7FFF) << 16) | ByteUtil.bytesToShort(buffer, start + 2));
        }
        return len;
    }

    private String genTableFormat() {
        StringBuilder mTableCharBuilder = new StringBuilder();
        for (int i = 0; i < mTableCharDepth; ++i) {
            mTableCharBuilder.append("\t");
        }

        return mTableCharBuilder.toString();
    }

    private void parseXmlNameSpace(BufferedInputStream io, long skip) throws Throwable {
        byte[] nodeBuf = new byte[AndroidXML.RES_TREE_NODE_SIZE + AndroidXML.RES_TREE_NAMESPACE_EXT_SIZE];
        if (skip > 0) {
            io.skip(skip);
        }
        int readSize = io.read(nodeBuf, 0, nodeBuf.length);
        if (readSize > 0) {
            ResXMLTreeNode node = new ResXMLTreeNode();
            node.type = ByteUtil.bytesToShort(nodeBuf, 0);
            node.headerSize = ByteUtil.bytesToShort(nodeBuf, 2);
            node.size = ByteUtil.bytesToInteger(nodeBuf, 4);
            node.lineNumber = ByteUtil.bytesToInteger(nodeBuf, 8);
            node.comment = new ResStringPoolRef();
            node.comment.index = ByteUtil.bytesToInteger(nodeBuf, 12);
            ResXMLTreeNameSpaceExt ext = new ResXMLTreeNameSpaceExt();
            ext.prefix = new ResStringPoolRef();
            ext.prefix.index = ByteUtil.bytesToInteger(nodeBuf, 16);
            ext.uri = new ResStringPoolRef();
            ext.uri.index = ByteUtil.bytesToInteger(nodeBuf, 20);

            mPrefix = mStringPoolList.get(ext.prefix.index).content;
            mUri = mStringPoolList.get(ext.uri.index).content;
        }
        io.reset();
    }

    private void parseXmlStartElement(BufferedInputStream io, long skip, APKCommonInfo info) throws Throwable {
        String tableChars = genTableFormat();
        mTableCharDepth++;
        byte[] attrBuf = new byte[AndroidXML.RES_TREE_NODE_SIZE + AndroidXML.RES_XML_TREE_ATTR_EXT_SIZE];
        if (skip > 0) {
            io.skip(skip);
        }
        int readSize = io.read(attrBuf, 0, attrBuf.length);
        if (readSize > 0) {
            ResXMLTreeAttrExt ext = new ResXMLTreeAttrExt();
            ext.ns = new ResStringPoolRef();
            ext.ns.index = ByteUtil.bytesToInteger(attrBuf, 16);
            ext.name = new ResStringPoolRef();
            ext.name.index = ByteUtil.bytesToInteger(attrBuf, 20);
            ext.attributeStart = ByteUtil.bytesToShort(attrBuf, 24);
            ext.attributeSize = ByteUtil.bytesToShort(attrBuf, 26);
            ext.attributeCount = ByteUtil.bytesToShort(attrBuf, 28);
            ext.idIndex = ByteUtil.bytesToShort(attrBuf, 30);
            ext.classIndex = ByteUtil.bytesToShort(attrBuf, 32);
            ext.styleIndex = ByteUtil.bytesToShort(attrBuf, 34);

            String rootElementName = mStringPoolList.get(ext.name.index).content;
            saveAndroidManifestInfo(tableChars + "<" + rootElementName);
            if (mIsHeaderFlag) {
                saveAndroidManifestInfo("\txmlns:" + mPrefix + "=\"" + mUri + "\"");
                mIsHeaderFlag = false;
            }

            if (ext.attributeCount > 0) {
                int attributeSize = ext.attributeCount * AndroidXML.RES_XML_TREE_ATTRIBUTE_SIZE;
                byte[] attributes = new byte[attributeSize];
                tableChars = genTableFormat();
                io.read(attributes, 0, attributeSize);
                saveAndroidManifestInfo("\n");
                for (int i = 0; i < ext.attributeCount; ++i) {
                    ResXMLTreeAttribute attribute = new ResXMLTreeAttribute();
                    attribute.ns = new ResStringPoolRef();
                    attribute.name = new ResStringPoolRef();
                    attribute.rawValue = new ResStringPoolRef();
                    attribute.typedValue = new ResValue();

                    int start = i * AndroidXML.RES_XML_TREE_ATTRIBUTE_SIZE;
                    attribute.ns.index = ByteUtil.bytesToInteger(attributes, start);
                    attribute.name.index = ByteUtil.bytesToInteger(attributes, start + 4);
                    attribute.rawValue.index = ByteUtil.bytesToInteger(attributes, start + 8);
                    attribute.typedValue.size = ByteUtil.bytesToShort(attributes, start + 12);
                    attribute.typedValue.res0 = attributes[start + 14];
                    attribute.typedValue.dataType = attributes[start + 15];
                    attribute.typedValue.data = ByteUtil.bytesToInteger(attributes, start + 16);

                    boolean isLast = i == ext.attributeCount - 1;
                    int empty = 0xFFFFFFFF;
                    if (attribute.ns.index != empty) {
                        saveAndroidManifestInfo(tableChars + mPrefix + ":" + mStringPoolList.get(attribute.name.index).content
                                + "=\"" + getAttributeValue(attribute.typedValue) + "\""
                                + (isLast ? ">\n" : "\n"));
                    } else {
                        saveAndroidManifestInfo(tableChars + mStringPoolList.get(attribute.name.index).content
                                + "=\"" + getAttributeValue(attribute.typedValue) + "\""
                                + (isLast ? ">\n" : "\n"));
                    }
                    saveCommonInfo(info, rootElementName, mStringPoolList.get(attribute.name.index).content, getAttributeValue(attribute.typedValue));
                }
            } else {
                saveAndroidManifestInfo("\n");
            }
        }
        io.reset();
    }

    private void saveCommonInfo(APKCommonInfo info, String rootElementName, String name, String value) {
        switch (name) {
            case APKCommonInfo.VERSION_CODE:
                info.setVersionCode(value);
                break;

            case APKCommonInfo.VERSION_NAME:
                info.setVersionName(value);
                break;

            case APKCommonInfo.PACKAGE:
                info.setPackageName(value);
                break;

            case APKCommonInfo.MIN_SDK_VERSION:
                info.setMinSdkVersion(Integer.parseInt(value));
                break;

            case APKCommonInfo.TARGET_SDK_VERSION:
                info.setTargetSdkVersion(Integer.parseInt(value));
                break;

            case APKCommonInfo.LABEL:
                if (APKCommonInfo.APPLICATION.equals(rootElementName)) {
                    info.setLabel(value);
                }
                break;

            default:
                break;
        }

        if (value.startsWith(APKCommonInfo.PERMISSION_START_TAG) && info.getPermissionList() != null) {
            info.getPermissionList().add(value);
        }
    }

    private void parseXmlEndElement(BufferedInputStream io, long skip) throws Throwable {
        --mTableCharDepth;
        String tableChars = genTableFormat();
        byte[] attrBuf = new byte[AndroidXML.RES_TREE_NODE_SIZE + AndroidXML.RES_XML_TREE_END_ELEMENT_EXT_SIZE];
        if (skip > 0) {
            io.skip(skip);
        }
        int readSize = io.read(attrBuf, 0, attrBuf.length);
        if (readSize > 0) {
            ResXMLTreeEndElementExt ext = new ResXMLTreeEndElementExt();
            ext.ns = new ResStringPoolRef();
            ext.ns.index = ByteUtil.bytesToInteger(attrBuf, 16);
            ext.name = new ResStringPoolRef();
            ext.name.index = ByteUtil.bytesToInteger(attrBuf, 20);

            saveAndroidManifestInfo(tableChars + "</" + mStringPoolList.get(ext.name.index).content + ">\n");
        }
        io.reset();
    }

    String getPackage(int data) {
        return ((data >> 24) == 1) ? "android" : "";
    }

    private float complexToFloat(int complex) {
        return (float)(complex & 0xFFFFFF00) * RADIX_MULTS[(complex >> 4) & 3];
    }

    private String getAttributeValue(ResValue value) {
        switch (value.dataType) {
            case TYPE_STRING:
                return mStringPoolList.get(value.data).content;

            case TYPE_ATTRIBUTE:
                return "?" + getPackage(value.data) + Integer.toHexString(value.data);

            case TYPE_REFERENCE:
                return "@" + getPackage(value.data) + Integer.toHexString(value.data);

            case TYPE_INT_HEX:
                return "0x" + Integer.toHexString(value.data);

            case TYPE_INT_BOOLEAN:
                return value.data == 0 ? "false" : "true";

            case TYPE_INT_DEC:
                return Integer.toString(value.data);

            default:
                break;
        }

        if (value.dataType >= TYPE_FIRST_COLOR_INT && value.dataType <= TYPE_LAST_COLOR_INT) {
            return "#" + Integer.toHexString(value.data);
        }

        if (value.dataType == TYPE_DIMENSION) {
            return complexToFloat(value.data) + DIMENSION_UNITS[value.data & COMPLEX_UNIT_MASK];
        }

        return "";
    }

    private File decompressAPKFile(File apk) {
        try {
            ZipFile zipFile = new ZipFile(apk);
            Enumeration entries = zipFile.entries();

            ZipEntry entry;
            while (entries.hasMoreElements()) {
                entry = (ZipEntry) entries.nextElement();
                if (entry != null) {
                    //System.out.println("name : " + entry.getName());
                    if (entry.getName().equals(MANIFEST_FILENAME)) {
                        File target = new File(entry.getName());
                        boolean delSuccess = true;
                        if (target.exists()) {
                            delSuccess = target.delete();
                        }

                        if (delSuccess) {
                            boolean success = target.createNewFile();
                            if (success) {
                                InputStream is = zipFile.getInputStream(entry);
                                FileOutputStream fos = new FileOutputStream(target);
                                int count;
                                byte[] buf = new byte[BUFFER_SIZE];
                                while ((count = is.read(buf)) != -1) {
                                    fos.write(buf, 0, count);
                                }
                                is.close();
                                fos.close();
                            }
                        }
                        return target;
                    }
                }
            }
        } catch (Throwable r) {
            r.printStackTrace();
        }

        return null;
    }

    private void deleteTargetFileBeforeParse() {
        File target = new File(TARGET_MANIFEST_FILENAME);
        if (target.exists()) {
            target.delete();
        }
    }

    private void saveAndroidManifestInfo(String info) {
        try {
            File target = new File(TARGET_MANIFEST_FILENAME);
            boolean success = true;
            if (!target.exists()) {
                success = target.createNewFile();
            }

            if (success) {
                FileOutputStream out = null;
                try {
                    out = new FileOutputStream(target, true);
                    out.write(info.getBytes());
                    out.close();
                } catch (Throwable r) {
                    r.printStackTrace();
                } finally {
                    if (out != null) {
                        try {
                            out.close();
                        } catch (Throwable r) {
                            r.printStackTrace();
                        }
                    }
                }
            }
        } catch (Throwable r) {
            r.printStackTrace();
        }
    }
}
